<template>
    <div class="h-100 w-100 flex-center" v-if="project">
        <template v-if="validProject">
            <el-form label-width="auto">
                <el-card shadow="never">
                    <template #header>
                        <el-form-item label="pom文件" class="mb-0">
                            <div class="flex-v-center">
                                <div>{{ path }}</div>
                                <tnxel-button type="primary"
                                    link
                                    icon="Refresh"
                                    class="ms-3"
                                    tooltip="重新加载"
                                    @click="load(path)"
                                />
                                <tnxel-button type="primary"
                                    link
                                    icon="FolderOpened"
                                    class="mx-2"
                                    tooltip="打开其它工程"
                                    @click="openNew"
                                />
                            </div>
                        </el-form-item>
                    </template>
                    <tnxel-alert type="warning" v-if="parentPomPath">这不是根工程，请打开一个Maven根工程</tnxel-alert>
                    <tnxel-alert type="success" v-else-if="truenewx">
                        这是truenewx框架工程，只可查看。请打开业务根工程的pom.xml文件
                    </tnxel-alert>
                    <el-form-item label="工程">
                        <MavenProjectPath :project="project"/>
                        <el-tooltip content="更改版本号" placement="top"
                            v-if="suitableModelParent && !model.versionUpdating">
                            <tnxel-icon class="ms-2 link" value="Edit" @click="toggleUpdateVersion"/>
                        </el-tooltip>
                        <div class="flex-v-center" v-else-if="model.versionUpdating">
                            <tnxel-icon class="text-secondary mx-2" value="Right"/>
                            <el-input v-model="model.version" style="width: 120px;" placeholder="请输入">
                                <template #append>
                                    <tnxel-button icon="Close" @click="toggleUpdateVersion"/>
                                </template>
                            </el-input>
                        </div>
                    </el-form-item>
                    <el-form-item label="打包方式">
                        <div class="flex-v-center">
                            <span>{{ project.packaging }}</span>
                            <div class="flex-v-center text-warning ms-2" v-if="project.packaging !== 'pom'">
                                <tnxel-icon value="WarningFilled"/>
                                <span>打包方式应该为 pom</span>
                                <span class="ms-2 link" @click="updatePackaging">修正</span>
                            </div>
                            <tnxel-icon class="text-success mx-2" value="CircleCheck" v-else-if="!truenewx"/>
                        </div>
                    </el-form-item>
                    <el-form-item label="父工程" v-if="project.packaging === 'pom' || parentPomPath">
                        <span class="text-secondary" v-if="!model.parent">无</span>
                        <el-link type="primary" :underline="false" @click="loadParent" v-else-if="parentPomPath">
                            <MavenProjectPath :project="model.parent"/>
                        </el-link>
                        <MavenProjectPath :project="model.parent" v-else/>

                        <el-dropdown trigger="click"
                            :class="suitableModelParent ? 'ms-2' : 'mx-2'"
                            v-if="project.packaging === 'pom' && !parentPomPath && !truenewx">
                            <span class="el-dropdown-link">
                                <el-link type="primary" :underline="false">
                                    <tnxel-icon value="CaretBottom"/>
                                </el-link>
                            </span>
                            <template #dropdown>
                                <el-dropdown-menu>
                                    <tnxel-dropdown-item
                                        v-for="(truenewxProject, index) of truenewxProjects" :key="index"
                                        :class="{'text-primary': projectEquals(truenewxProject, model.parent)}"
                                        :icon="projectEquals(truenewxProject, model.parent) ? 'Select': 'fa'"
                                        @click="changeModelParent(truenewxProject)"
                                    >
                                        <div class="flex-v-center">
                                            <MavenProjectPath :project="truenewxProject" class="me-1"/>
                                            <div class="ms-2 text-secondary">({{ truenewxProject.arch }})</div>
                                        </div>
                                    </tnxel-dropdown-item>
                                </el-dropdown-menu>
                            </template>
                        </el-dropdown>

                        <tnxel-icon class="text-success mx-2" value="CircleCheck" v-if="suitableModelParent"/>
                        <el-tooltip placement="top" content="不是有效的父工程，请选择一个有效父工程"
                            v-else-if="!parentPomPath && !truenewx">
                            <tnxel-icon class="text-warning cursor-pointer" value="WarningFilled"/>
                        </el-tooltip>
                    </el-form-item>
                    <template v-if="project.packaging === 'pom'">
                        <el-form-item label="体系架构" v-if="model.parent && typeof model.singleArch === 'boolean'">
                            {{ TruenewxProjectUtil.arch[model.parent.artifactId] }}
                        </el-form-item>
                        <el-form-item label="JDK版本" v-if="model.jdkVersion || requiredJdkVersion">
                            <span v-if="model.jdkVersion && model.jdkVersion !== requiredJdkVersion">
                                {{ model.jdkVersion }}
                            </span>
                            <template v-else-if="requiredJdkVersion">
                                <span class="me-2">{{ requiredJdkVersion }}</span>
                                <span class="text-placeholder">(由父工程决定)</span>
                            </template>

                            <el-dropdown trigger="click" v-if="!truenewx && availableJdkVersions.length">
                                <span class="el-dropdown-link">
                                    <el-link type="primary" :underline="false">
                                        <tnxel-icon class="ms-2" value="CaretBottom"/>
                                    </el-link>
                                </span>
                                <template #dropdown>
                                    <el-dropdown-menu>
                                        <el-dropdown-item disabled>仅限LTS版本</el-dropdown-item>
                                        <tnxel-dropdown-item
                                            v-for="(availableJdkVersion, index) of availableJdkVersions"
                                            :key="index" :divided="index === 0"
                                            :class="{'text-primary': (model.jdkVersion ? model.jdkVersion === availableJdkVersion : requiredJdkVersion === availableJdkVersion)}"
                                            :icon="(model.jdkVersion ? model.jdkVersion === availableJdkVersion : requiredJdkVersion === availableJdkVersion) ? 'Select': 'fa'"
                                            @click="changeModelJdkVersion(availableJdkVersion)">
                                            <span>{{ availableJdkVersion }}</span>
                                            <span class="text-placeholder ms-2"
                                                v-if="availableJdkVersion === requiredJdkVersion">(默认)</span>
                                        </tnxel-dropdown-item>
                                    </el-dropdown-menu>
                                </template>
                            </el-dropdown>

                            <tnxel-icon class="text-success mx-2" value="CircleCheck"
                                v-if="validModelJdkVersion"/>
                            <el-tooltip placement="top"
                                :content="'父工程要求JDK最低版本为' + requiredJdkVersion"
                                v-else-if="suitableModelParent">
                                <tnxel-icon class="text-warning ms-2 cursor-pointer" value="WarningFilled"/>
                            </el-tooltip>
                        </el-form-item>
                        <el-form-item label="层级" :key="parseTime" v-if="model.singleArch === true">
                            <div class="flex-v-center" v-if="model.modules.length">
                                <el-tag v-for="module of model.modules" :key="module.artifactId"
                                    type="info"
                                    class="me-2"
                                >
                                    {{ getLayerKey(module.artifactId) }}
                                </el-tag>
                                <tnxel-button
                                    type="primary"
                                    link
                                    icon="Edit"
                                    tooltip="更改标准层级"
                                    @click="toUpdateLayers"
                                />
                            </div>
                            <tnxel-button icon="Plus" @click="toAddLayers" v-else>添加标准层级</tnxel-button>
                        </el-form-item>
                        <el-form-item label="模块" v-else-if="model.singleArch === false">
                            <div class="w-100">
                                <el-tree
                                    class="border rounded py-1"
                                    style="max-height: 256px; overflow-y: auto;"
                                    :key="parseTime"
                                    :data="validModelModules"
                                    node-key="name"
                                    :props="{
                                    label: 'artifactId',
                                    children: 'modules'
                                }"
                                    empty-text="无模块"
                                    :expand-on-click-node="false"
                                >
                                    <template #default="{data}">
                                        <div class="flex-v-center flex-1">
                                            <tnxel-icon value="bi bi-folder" v-if="data.modules?.length"/>
                                            <tnxel-icon value="bi bi-file-earmark-text-fill" v-else/>
                                            <div class="ms-1 flex-1">
                                                <span>{{ data.artifactId }}</span>
                                                <span class="text-placeholder ms-2" v-if="data.basic">(基础模块)</span>
                                            </div>
                                            <el-tooltip :content="'更改 ' + data.artifactId" placement="top">
                                                <tnxel-icon class="link ms-2" value="Edit"
                                                    @click="toUpdateModule(data)"/>
                                            </el-tooltip>
                                            <el-tooltip :content="'移除 ' + data.artifactId" placement="top">
                                                <tnxel-icon class="link mx-2" value="CircleClose"
                                                    @click="toRemoveModule(data)"/>
                                            </el-tooltip>
                                        </div>
                                    </template>
                                </el-tree>
                                <div class="mt-2 w-100">
                                    <el-row :gutter="12" v-if="suitableModelParent && !truenewx">
                                        <el-col :span="12">
                                            <tnxel-button
                                                class="w-100"
                                                icon="bi bi-folder-plus"
                                                @click="toAddModule(false)"
                                            >
                                                添加标准模块
                                            </tnxel-button>
                                        </el-col>
                                        <el-col :span="12">
                                            <tnxel-button
                                                class="w-100"
                                                icon="bi bi-file-earmark-plus-fill"
                                                @click="toAddModule(true)"
                                            >
                                                添加简单模块
                                            </tnxel-button>
                                        </el-col>
                                    </el-row>
                                </div>
                            </div>
                        </el-form-item>
                        <el-form-item label=" " v-if="!parentPomPath && !truenewx">
                            <tnxel-button
                                type="primary"
                                icon="bi bi-save"
                                :disabled="!suitableModelParent"
                                @click="save"
                            >
                                保存
                            </tnxel-button>
                        </el-form-item>
                    </template>
                </el-card>
                <div class="w-100 flex-center my-4">
                    <tnxel-button
                        type="primary"
                        plain
                        size="large"
                        icon="bi bi-layout-text-window-reverse"
                        @click="toModeling"
                        v-if="project.modules.length">
                        进入建模视图
                    </tnxel-button>
                </div>
            </el-form>
        </template>
        <tnxel-alert class="w-fit-content" type="error" v-else>不是有效的Maven工程配置文件</tnxel-alert>
    </div>
</template>

<script>
import tnx from '@/tnx.js';
import PomUtil from '@/logic/maven/util/pom.js';
import ProjectUtil from '@/logic/maven/util/project.js';
import TruenewxProjectUtil from '@/logic/maven/util/truenewx-project.js';
import ModuleLayer from '@/logic/maven/enums/ModuleLayer.js';
import RepoType from '@/logic/maven/enums/RepoType.js';
import CodingType from '@/logic/coding/model/CodingType.js';
import CodingClass from '@/logic/coding/model/CodingClass.js';
import CodingMethod from '@/logic/coding/model/CodingMethod.js';

import MavenProjectPath from './components/MavenProjectPath.vue';
import LayerForm from './components/LayerForm.vue';
import simpleModuleAdd from './simple-module-add.vue';
import simpleModuleUpdate from './simple-module-update.vue';
import pomModuleAdd from './pom-module-add.vue';
import pomModuleUpdate from './pom-module-update.vue';
import open from './open.js';

export default {
    components: {MavenProjectPath},
    data() {
        return {
            path: null,
            xml: null,
            project: null,
            model: {
                parent: null,
                singleArch: null,
                groupId: null,
                artifactId: null,
                version: null,
                versionUpdating: false,
                jdkVersion: null,
                modules: [],
            },
            parentPomPath: null,
            parseTime: new Date().getTime(),
            auto: false, // 是否自动跳转到建模视图
        };
    },
    computed: {
        TruenewxProjectUtil() {
            return TruenewxProjectUtil;
        },
        validProject() {
            return this.project && this.project.artifactId;
        },
        truenewx() {
            return this.project && this.project.groupId === TruenewxProjectUtil.groupId && this.project.artifactId === TruenewxProjectUtil.artifactId;
        },
        truenewxProjects() {
            let projects = [];
            for (let module of TruenewxProjectUtil.modules) {
                let versions = Object.keys(TruenewxProjectUtil.versions);
                for (let version of versions) {
                    projects.push({
                        groupId: TruenewxProjectUtil.groupId,
                        artifactId: module,
                        version: version,
                        arch: TruenewxProjectUtil.arch[module],
                    });
                }
            }
            return projects;
        },
        suitableModelParent() {
            return this.model.parent && this.model.parent.groupId === TruenewxProjectUtil.groupId;
        },
        projectJdkVersion() {
            let properties = this.project.properties;
            if (properties) {
                let jdkVersion = properties['maven.compiler.source'] || properties['project.java.version'];
                if (jdkVersion) {
                    return parseInt(jdkVersion);
                }
            }
            return undefined;
        },
        requiredJdkVersion() {
            if (this.model.parent) {
                let version = TruenewxProjectUtil.versions[this.model.parent.version];
                return parseInt(version);
            }
            return undefined;
        },
        availableJdkVersions() {
            let versions = [11, 17, 21];
            versions.removeIf(version => version < this.requiredJdkVersion);
            return versions;
        },
        validModelJdkVersion() {
            let jdkVersion = this.model.jdkVersion || this.requiredJdkVersion;
            return this.requiredJdkVersion && jdkVersion && jdkVersion >= this.requiredJdkVersion;
        },
        validModelModules() {
            let modules = this.model.modules ? this.model.modules.filter(m => !m.removed) : [];
            modules.sort((m1, m2) => m1.artifactId.localeCompare(m2.artifactId));
            return modules;
        },
        completed() {
            return this.validProject && this.suitableModelParent && this.validModelJdkVersion !== false // validJdkVersion可能为undefined，此时有效
                && this.project.packaging === 'pom' && this.project.singleArch !== undefined;
        },
    },
    watch: {
        parseTime() {
            let singleArch = ProjectUtil.isSingleArch(this.project);
            if (singleArch === undefined) { // 未决
                this.auto = false; // 停止自动跳转
                delete this.project.singleArch;
            } else {
                this.project.singleArch = singleArch;
                this.autoJump();
            }
        },
    },
    mounted() {
        this.auto = this.$route.query.auto === 'true';
        let path = this.$route.query.path;
        if (path) {
            this.load(path);
        }
    },
    methods: {
        getLayerKey(artifactId) {
            return ProjectUtil.getLayerKey(artifactId);
        },
        openNew() {
            open(this, false);
        },
        load(path) {
            tnx.util.file.read(path).then(xml => {
                this.path = path;
                this.xml = xml;
                this.refresh();
            });
        },
        refresh() {
            tnx.showLoading().then(() => {
                ProjectUtil.parseProject(this.path, this.xml, project => {
                    this.project = project;

                    this.model.parent = tnx.util.object.deepClone(this.project.parent);
                    this.model.singleArch = ProjectUtil.isSingleArch(this.project);
                    this.model.groupId = this.project.groupId;
                    this.model.artifactId = this.project.artifactId;
                    this.model.version = this.project.version;
                    this.model.jdkVersion = this.projectJdkVersion;
                    this.model.modules = tnx.util.object.deepClone(project.modules);
                    if (this.model.modules) {
                        for (let module of this.model.modules) {
                            module.basic = ProjectUtil.isBasic(module);
                        }
                    }

                    this.parseTime = new Date().getTime();

                    // 判断是否根工程
                    if (this.project.parent?.groupId) {
                        let parentDir = tnx.util.file.getParentDir(this.path);
                        if (parentDir) {
                            parentDir = tnx.util.file.getParentDir(parentDir);
                            if (parentDir) {
                                let parentPomPath = parentDir + 'pom.xml';
                                tnx.util.file.exists(parentPomPath).then(exists => {
                                    if (exists) {
                                        this.parentPomPath = parentPomPath;
                                    } else {
                                        this.parentPomPath = null;
                                    }
                                });
                            }
                        }
                    }
                    tnx.hideLoading();
                });
            });
        },
        loadParent() {
            this.load(this.parentPomPath);
        },
        toggleUpdateVersion() {
            this.model.versionUpdating = !this.model.versionUpdating;
            this.model.version = this.project.version; // 无论是开启还是关闭更改，都同步工程里的版本号
        },
        updateModuleVersion(module, parentDir, oldVersion, newVersion) {
            let modulePomPath = tnx.util.file.joinPath(parentDir, module.artifactId, 'pom.xml');
            tnx.util.file.read(modulePomPath).then(modulePomXml => {
                modulePomXml = PomUtil.replaceParentVersion(modulePomXml, module, newVersion);
                modulePomXml = PomUtil.replaceProjectVersion(modulePomXml, oldVersion, newVersion);
                tnx.util.file.write(modulePomPath, modulePomXml).then(() => {
                    module.version = newVersion;

                    for (let subModule of module.modules) {
                        let subParentDir = tnx.util.file.joinPath(parentDir, module.artifactId);
                        this.updateModuleVersion(subModule, subParentDir, oldVersion, newVersion);
                    }
                });
            });
        },
        projectEquals(p1, p2) {
            return p1 && p2 && p1.groupId === p2.groupId && p1.artifactId === p2.artifactId && p1.version === p2.version;
        },
        changeModelParent(parentProject) {
            this.model.parent = parentProject;
            this.model.singleArch = parentProject.artifactId === 'tnxjee';
        },
        updatePackaging() {
            if (this.project.packaging !== 'pom') {
                let packagingXml = '<packaging>pom</packaging>';
                // 移除可能存在且位置错误的 packaging，再在顶层 version/artifactId 之后插入
                let xml = this.xml;
                if (xml.includes('<packaging>')) {
                    xml = xml.replace(/\s*<packaging>[^<]*<\/packaging>\s*/g, '\n');
                }

                const parentEnd = xml.indexOf('</parent>');
                const searchStart = parentEnd >= 0 ? parentEnd + '</parent>'.length : 0;
                const versionStart = xml.indexOf('<version>', searchStart);
                let insertPos = -1;
                if (versionStart >= 0) {
                    const versionEnd = xml.indexOf('</version>', versionStart);
                    if (versionEnd >= 0) {
                        insertPos = versionEnd + '</version>'.length;
                    }
                } else {
                    const artifactStart = xml.indexOf('<artifactId>', searchStart);
                    if (artifactStart >= 0) {
                        const artifactEnd = xml.indexOf('</artifactId>', artifactStart);
                        if (artifactEnd >= 0) {
                            insertPos = artifactEnd + '</artifactId>'.length;
                        }
                    }
                }

                if (insertPos >= 0) {
                    const indent = '    ';
                    xml = xml.slice(0, insertPos) + '\n' + indent + packagingXml + xml.slice(insertPos);
                } else {
                    xml = tnx.util.xml.setTag(xml, 'packaging', packagingXml, ['version', 'artifactId']);
                }
                tnx.util.file.write(this.path, xml).then(() => {
                    this.xml = xml;
                    this.project.packaging = 'pom';
                    tnx.toast('已保存');
                }).catch(error => {
                    tnx.error(error);
                });
            }
        },
        changeModelJdkVersion(newJdkVersion) {
            this.model.jdkVersion = newJdkVersion;
        },
        replaceProjectPropertiesXml(xml, properties) {
            if (properties && typeof properties === 'object') {
                let propertiesXml = '<properties>\n';
                let keys = Object.keys(properties);
                if (keys.length) {
                    for (let key of keys) {
                        propertiesXml += '        <' + key + '>' + properties[key] + '</' + key + '>\n';
                    }
                    propertiesXml += '    </properties>';
                } else {
                    propertiesXml = '';
                }
                xml = tnx.util.xml.setTag(xml, 'properties', propertiesXml, ['modules', 'parent']);
            }
            return xml;
        },
        toAddLayers() {
            tnx.open(LayerForm, {
                root: this.model,
                parent: this.model,
                after: modules => {
                    this.model.modules = modules;
                }
            }, {
                title: '添加标准层级',
            });
        },
        toUpdateLayers() {
            tnx.open(LayerForm, {
                root: this.model,
                parent: this.model,
                after: modules => {
                    this.model.modules = modules;
                }
            }, {
                title: '更改标准层级',
            });
        },
        toAddModule(simple) {
            if (simple) {
                tnx.open(simpleModuleAdd, {
                    parent: this.model,
                    after: module => {
                        this.model.modules.push(module);
                    }
                });
            } else {
                tnx.open(pomModuleAdd, {
                    parent: this.model,
                    parentPomPath: this.path,
                    after: module => {
                        let removedModule = this.model.modules.find(
                            m => m.removed && m.artifactId === module.artifactId);
                        if (removedModule) {
                            Object.assign(removedModule, module);
                            removedModule.removed = false;
                        } else {
                            this.model.modules.push(module);
                        }
                    }
                });
            }
        },
        toUpdateModule(module) {
            if (module.modules?.length) {
                tnx.open(pomModuleUpdate, {
                    parent: this.model,
                    parentPomPath: this.path,
                    module: module,
                    after: newModule => {
                        Object.assign(module, newModule);
                    }
                });
            } else {
                tnx.open(simpleModuleUpdate, {
                    parent: this.model,
                    module: module,
                    after: newModule => {
                        Object.assign(module, newModule);
                    }
                });
            }
        },
        toRemoveModule(module) {
            let message = '确定要移除模块 ' + module.artifactId + ' 吗？';
            if (module.basic) {
                message += '（该模块为基础模块）';
            }
            tnx.confirm(message).then(yes => {
                if (yes) {
                    module.removed = true;
                }
            });
        },
        autoJump() {
            if (this.auto) {
                this.$nextTick(() => {
                    if (this.completed) {
                        this.toModeling();
                    }
                });
            }
        },
        save() {
            if (!this.model.parent) {
                return;
            }
            let xml = this.xml;

            // 为了尽可能地不改动原pom.xml文件的格式，采用字符串替换的方式生成新的内容，而不是通过格式化xml结构的方式
            if (this.model.version !== this.model.parent.version) {
                let versionXml = '<version>' + this.model.version + '</version>';
                xml = tnx.util.xml.setTag(xml, 'version', versionXml, ['artifactId']);
            }

            let parentXml = '<parent>\n';
            parentXml += '        <groupId>' + this.model.parent.groupId + '</groupId>\n';
            parentXml += '        <artifactId>' + this.model.parent.artifactId + '</artifactId>\n';
            parentXml += '        <version>' + this.model.parent.version + '</version>\n';
            parentXml += '    </parent>';
            xml = tnx.util.xml.setTag(xml, 'parent', parentXml, ['modelVersion', 'packaging', 'version', 'artifactId']);

            let modulesXml = '';
            if (this.validModelModules.length) {
                modulesXml = PomUtil.getModulesXml(this.validModelModules, 4);
            }
            xml = tnx.util.xml.setTag(xml, 'modules', modulesXml, ['packaging']);

            let properties = Object.assign({}, this.project.properties, {
                'maven.build.timestamp.format': 'yyMMddHHmmss',
                'project.build.timestamp': '${maven.build.timestamp}',
                'truenewx.version': this.model.parent.version,
            });
            // jdk版本
            if (this.model.jdkVersion !== this.projectJdkVersion) { // 有变更才保存
                delete properties['project.java.version']; // 不使用该属性记录jdk版本
                if (this.model.jdkVersion === this.requiredJdkVersion) { // 使用默认JDK版本则删除相应属性
                    delete properties['maven.compiler.source'];
                    delete properties['maven.compiler.target'];
                } else { // 使用非默认JDK版本则修改相应属性
                    Object.assign(properties, {
                        'maven.compiler.source': this.model.jdkVersion,
                        'maven.compiler.target': this.model.jdkVersion,
                    });
                }
            }
            xml = this.replaceProjectPropertiesXml(xml, properties);

            let buildXml = '<build>\n' +
                '        <resources>\n' +
                '            <resource>\n' +
                '                <directory>src/main/resources</directory>\n' +
                '                <filtering>true</filtering>\n' +
                '            </resource>\n' +
                '        </resources>\n' +
                '    </build>';
            xml = tnx.util.xml.setTag(xml, 'build', buildXml, ['properties']);

            tnx.util.file.write(this.path, xml).then(() => {
                this.xml = xml;

                let baseModule = this.validModelModules.find(m => m.basic);
                let dir = tnx.util.file.getParentDir(this.path);

                // 先删除已移除的模块，以免有效模块中有重名模块而被后续删除
                for (let removedModule of this.model.modules) {
                    if (removedModule.removed) {
                        let moduleDir = tnx.util.file.joinPath(dir, removedModule.artifactId);
                        tnx.util.file.delete(moduleDir, true);
                    }
                }

                this.saveModules(baseModule, dir, this.model, 0, 0, () => {
                    this.model.versionUpdating = false;
                    tnx.toast('已保存').then(() => {
                        this.refresh();
                    });
                });
            }).catch(error => {
                tnx.error(error);
            });
        },
        saveModules(baseModule, dir, parentProject, projectIndex, moduleIndex, callback) {
            let modules = parentProject.modules;
            if (modules.length && moduleIndex < modules.length) {
                let module = modules[moduleIndex];
                if (module.removed) {
                    this.saveModules(baseModule, dir, parentProject, projectIndex, moduleIndex + 1, callback);
                    return;
                }
                module.version = parentProject.version;
                let moduleDir = tnx.util.file.joinPath(dir, module.artifactId);
                let modulePomPath = tnx.util.file.joinPath(moduleDir, 'pom.xml');

                tnx.util.file.read(modulePomPath, false, '').then(modulePomXml => {
                    modulePomXml = PomUtil.roundXml(modulePomXml);

                    if (module.groupId !== parentProject.groupId) {
                        modulePomXml =
                            tnx.util.xml.setTag(modulePomXml, 'groupId', `<groupId>${module.groupId}</groupId>`,
                                ['modelVersion']);
                    }
                    modulePomXml =
                        tnx.util.xml.setTag(modulePomXml, 'artifactId', `<artifactId>${module.artifactId}</artifactId>`,
                            ['groupId', 'modelVersion']);
                    // 基础模块的子模块打包方式均为jar，不生成<packaging>
                    if (!parentProject.basic && module.packaging && module.packaging !== 'jar') {
                        modulePomXml =
                            tnx.util.xml.setTag(modulePomXml, 'packaging', `<packaging>${module.packaging}</packaging>`,
                                ['artifactId']);
                    } else {
                        modulePomXml = tnx.util.xml.setTag(modulePomXml, 'packaging', '', []);
                    }
                    let parentXml = '<parent>\n' +
                        PomUtil.getProjectXml(parentProject, 8) +
                        '    </parent>';
                    modulePomXml = tnx.util.xml.setTag(modulePomXml, 'parent', parentXml, ['packaging', 'artifactId']);
                    let properties = {};
                    if (module.basic) {
                        properties[ProjectUtil.BASIC_PROJECT_PROPERTY_KEY] = 'true';
                    }
                    modulePomXml = this.replaceProjectPropertiesXml(modulePomXml, properties);
                    if (module.packaging === 'pom') { // 简单的pom模块
                        let modulesXml = PomUtil.getModulesXml(module.modules, 4);
                        modulePomXml = tnx.util.xml.setTag(modulePomXml, 'modules', modulesXml, ['parent']);

                        tnx.util.file.write(modulePomPath, modulePomXml).then(() => {
                            if (module.modules?.length) {
                                this.saveModules(baseModule, moduleDir, module, projectIndex, 0, () => {
                                    this.saveModules(baseModule, dir, parentProject, projectIndex + 1, moduleIndex + 1,
                                        callback);
                                });
                            } else {
                                callback();
                            }
                        });
                    } else { // 以层级模块的方式保存
                        module.dependencies = module.dependencies || [];

                        let layer = ProjectUtil.getLayerKey(module.artifactId).toUpperCase();
                        // 如果有基础模块（其中有层级子模块），且当前层级不是api层，需处理对基础模块的依赖
                        if (baseModule?.modules?.length && layer !== ModuleLayer.API.name) {
                            let baseLayerModule = baseModule.modules.find(
                                m => m.artifactId.endsWith('-' + layer.toLowerCase()));
                            if (baseLayerModule) { // 存在基础模块
                                // 当前层级模块就是基础层级模块，则去掉对所有其它模块层级的依赖
                                if (module.groupId === baseLayerModule.groupId && module.artifactId === baseLayerModule.artifactId) {
                                    module.dependencies.removeIf(dependency => {
                                        if (dependency.groupId !== module.groupId) {
                                            if (ProjectUtil.findModule(this.model, dependency)) {
                                                return true;
                                            }
                                        }
                                        // 对相同模块内其它层级模块的依赖保留
                                        return false;
                                    });
                                } else { // 当前层级模块不是基础层级模块
                                    // 先去掉基础层级模块中已有的依赖库（test范围的除外）和对基础层级模块的依赖，后者后续会再加上
                                    module.dependencies.removeIf(md => (md.groupId === baseLayerModule.groupId
                                            && md.artifactId === baseLayerModule.artifactId) ||
                                        (md.scope !== 'test' && baseLayerModule.dependencies.some(
                                            bd => md.groupId === bd.groupId && md.artifactId === bd.artifactId)));
                                    // 如果当前层级模块依赖其它模块相同层级，则去掉对基础模块相同层级的依赖
                                    if (ProjectUtil.dependsOtherModuleLayer(this.model, module)) {
                                        module.dependencies.remove(dependency =>
                                            dependency.groupId === baseLayerModule.groupId
                                            && dependency.artifactId === baseLayerModule.artifactId);
                                    } else { // 当前层级模块不依赖其它模块相同层级，则加入对基础模块相同层级的依赖
                                        module.dependencies.push({
                                            groupId: baseLayerModule.groupId,
                                            artifactId: baseLayerModule.artifactId,
                                            version: '${project.version}',
                                        });
                                    }
                                }
                            }
                        }
                        if (layer === ModuleLayer.WEB.name) {
                            // 基础模块的web层不生成build节点
                            if (parentProject.basic) {
                                modulePomXml = tnx.util.xml.setTag(modulePomXml, 'build', '', []);
                            } else {
                                let outputDir = '../target';
                                if (!this.model.singleArch) {
                                    outputDir = '../' + outputDir;
                                }
                                let buildXml = '<build>\n' +
                                    `        <finalName>${parentProject.artifactId}</finalName>\n` +
                                    '        <plugins>\n' +
                                    '            <plugin>\n' +
                                    '                <groupId>org.springframework.boot</groupId>\n' +
                                    '                <artifactId>spring-boot-maven-plugin</artifactId>\n' +
                                    '                <configuration>\n' +
                                    `                    <outputDirectory>${outputDir}</outputDirectory>\n` +
                                    '                </configuration>\n' +
                                    '            </plugin>\n' +
                                    '            <plugin>\n' +
                                    '                <groupId>org.apache.maven.plugins</groupId>\n' +
                                    '                <artifactId>maven-resources-plugin</artifactId>\n' +
                                    '                <configuration>\n' +
                                    '                    <encoding>UTF-8</encoding>\n' +
                                    '                    <nonFilteredFileExtensions>\n' +
                                    '                        <nonFilteredFileExtension>ttf</nonFilteredFileExtension>\n' +
                                    '                        <nonFilteredFileExtension>woff</nonFilteredFileExtension>\n' +
                                    '                        <nonFilteredFileExtension>woff2</nonFilteredFileExtension>\n' +
                                    '                    </nonFilteredFileExtensions>\n' +
                                    '                </configuration>\n' +
                                    '            </plugin>\n' +
                                    '        </plugins>\n' +
                                    '    </build>';
                                modulePomXml = tnx.util.xml.setTag(modulePomXml, 'build', buildXml, ['parent']);
                            }
                        }
                        let dependenciesXml = PomUtil.getDependenciesXml(module.dependencies, 4);
                        modulePomXml = tnx.util.xml.setTag(modulePomXml, 'dependencies', dependenciesXml,
                            ['build', 'parent']);
                        tnx.util.file.write(modulePomPath, modulePomXml).then(() => {
                            // 当前模块即为基础模块，或者，整个工程没有基础模块且当前模块没有依赖其它模块，则为独立模块
                            let alone = (baseModule?.groupId === parentProject.groupId
                                    && baseModule?.artifactId === parentProject.artifactId) ||
                                (!baseModule && !ProjectUtil.dependsOtherModuleLayer(this.model, module));
                            if (layer === ModuleLayer.MODEL.name) {
                                this.saveProjectRootClass(moduleDir, !alone);
                                this.saveMavenProperties(moduleDir, !alone);
                            } else if (layer === ModuleLayer.REPO.name) {
                                this.saveRepoYaml(parentProject, moduleDir, module);
                            } else if (layer === ModuleLayer.SERVICE.name) {
                                this.saveServiceYml(parentProject, moduleDir);
                                this.saveLogbackXml(parentProject, moduleDir, !alone);
                                this.saveJunitContext(moduleDir, module);
                            } else if (layer === ModuleLayer.WEB.name) {
                                let appClassSimpleName = this.saveWebAppClass(baseModule, parentProject, moduleDir,
                                    module);
                                this.saveServletInitializer(parentProject, moduleDir, module, appClassSimpleName);
                                this.saveWebProperties(parentProject, projectIndex, moduleDir, module);
                                this.saveWebMvcConfigClass(baseModule, parentProject, moduleDir, module);
                                this.saveWebSecurityConfigClass(baseModule, parentProject, moduleDir, module);
                            }

                            this.saveModules(baseModule, dir, parentProject, projectIndex, moduleIndex + 1, callback);
                        });
                    }
                });
            } else {
                callback();
            }
        },
        saveProjectRootClass(moduleDir, toDelete) {
            let projectRootClass = this.getProjectRootClass();
            let projectRootClassPath = this.getJavaClassPath(moduleDir, projectRootClass.getName());
            if (toDelete) {
                tnx.util.file.delete(projectRootClassPath, true);
            } else { // 类文件不存在才生成，避免覆盖改动后的类
                tnx.util.file.exists(projectRootClassPath).then(exists => {
                    if (!exists) {
                        projectRootClass.addImportClass('org.springframework.boot.autoconfigure.SpringBootApplication');
                        projectRootClass.annotations = ['SpringBootApplication'];
                        tnx.util.file.write(projectRootClassPath, projectRootClass.toString());
                    }
                });
            }
        },
        getProjectRootClass() {
            let packageName = this.validModelModules[0].groupId;
            let simpleNames = this.model.artifactId.split('-');
            let simpleName = '';
            for (let sn of simpleNames) {
                simpleName += sn.firstToUpperCase();
            }
            simpleName += 'Root';
            return new CodingClass(packageName, simpleName);
        },
        saveMavenProperties(moduleDir, toDelete) {
            let mavenPropertiesPath = tnx.util.file.joinPath(moduleDir, 'src', 'main', 'resources', 'maven.properties');
            if (toDelete) {
                tnx.util.file.delete(mavenPropertiesPath, true);
            } else {
                tnx.util.file.read(mavenPropertiesPath, false, '').then(mavenPropertiesContent => {
                    let mavenProperties = new tnx.Properties(mavenPropertiesContent);
                    mavenProperties.setIfAbsent('project.build.timestamp.pattern', '@maven.build.timestamp.format@');
                    mavenProperties.setIfAbsent('project.version', '@project.version@.@project.build.timestamp@');
                    mavenPropertiesContent = mavenProperties.toString();
                    tnx.util.file.write(mavenPropertiesPath, mavenPropertiesContent);
                });
            }
        },
        saveRepoYaml(project, moduleDir, module) {
            let filenames = ['application.yaml'];
            if (project.basic) { // 基础模块将application配置文件至于config目录下
                filenames.unshift('config');
            }
            let yamlPath = this.getConfigResourcePath(moduleDir, filenames);
            tnx.util.file.read(yamlPath, false, '').then(yamlContent => {
                const yaml = new tnx.Yaml(yamlContent);
                if (!project.basic) {
                    yaml.setIfAbsent(['spring', 'application', 'name'], project.artifactId);
                }
                let repoTypes = ProjectUtil.getRepoTypes(module.dependencies);
                if (repoTypes.includes(RepoType.JPA.name) || repoTypes.includes(RepoType.MYBATIS.name)) {
                    yaml.setIfAbsent(['spring', 'datasource', 'driver-class-name'], 'com.mysql.cj.jdbc.Driver');
                    yaml.setIfAbsent(['spring', 'datasource', 'url'],
                        'jdbc:mysql://${datasource.host:localhost}:${datasource.port:3306}/' +
                        '${datasource.schema:${spring.application.name}}' +
                        '?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&keepAlive=true&autoReconnect=true&autoReconnectForPools=true');
                    yaml.setIfAbsent(['spring', 'datasource', 'username'], '${datasource.username:root}');
                    yaml.setIfAbsent(['spring', 'datasource', 'password'], '${datasource.password:123456}');
                    yaml.setIfAbsent(['spring', 'datasource', 'type'], 'com.zaxxer.hikari.HikariDataSource');
                    yaml.setIfAbsent(['spring', 'datasource', 'hikari', 'auto-commit'], true);
                    yaml.setIfAbsent(['spring', 'datasource', 'hikari', 'minimum-idle'], 3);
                    yaml.setIfAbsent(['spring', 'datasource', 'hikari', 'maximum-pool-size'], 100);
                    yaml.setIfAbsent(['spring', 'datasource', 'hikari', 'connection-test-query'], 'select 1');

                    yaml.setIfAbsent(['spring', 'flyway', 'table'], 'flyway_schema_history');
                    yaml.setIfAbsent(['spring', 'flyway', 'locations'],
                        'classpath:sql/increase,classpath:flyway/migration');
                    yaml.setIfAbsent(['spring', 'flyway', 'baseline-version'], '0.1.0');
                    yaml.setIfAbsent(['spring', 'flyway', 'sql-migration-prefix'], 'v');
                    yaml.setIfAbsent(['spring', 'flyway', 'baseline-on-migrate'], true);
                    yaml.setIfAbsent(['spring', 'flyway', 'validate-on-migrate'], false);
                    yaml.setIfAbsent(['spring', 'flyway', 'ignore-migration-patterns'], '*:missing');
                }
                if (repoTypes.includes(RepoType.JPA.name)) {
                    yaml.setIfAbsent(['spring', 'jpa', 'mapping-resources'], 'jpa/**/*.xml');
                    yaml.setIfAbsent(['spring', 'jpa', 'open-in-view'], true);
                    yaml.setIfAbsent(['spring', 'jpa', 'database'], 'MYSQL');
                    yaml.setIfAbsent(['spring', 'jpa', 'database-platform'], 'org.hibernate.dialect.MySQLDialect');
                    yaml.setIfAbsent(['spring', 'jpa', 'show-sql'], true);
                    yaml.setIfAbsent(['spring', 'jpa', 'hibernate', 'ddl-auto'], 'validate');
                }
                if (repoTypes.includes(RepoType.REDIS.name)) {
                    yaml.setIfAbsent(['spring', 'redis', 'host'], 'localhost');
                    yaml.setIfAbsent(['spring', 'redis', 'port'], '6379');
                    yaml.setIfAbsent(['spring', 'redis', 'username'], '""');
                    yaml.setIfAbsent(['spring', 'redis', 'password'], '""');
                }
                yamlContent = yaml.toString();
                tnx.util.file.write(yamlPath, yamlContent);
            });
        },
        saveLogbackXml(project, moduleDir, toDelete) {
            let springXmlFilename = 'logback-spring.xml';
            let springXmlPath = this.getConfigResourcePath(moduleDir, springXmlFilename);
            if (toDelete) {
                tnx.util.file.delete(springXmlPath, true);
            } else {
                tnx.util.file.read(springXmlPath, false, '').then(springXmlContent => {
                    // 文件不存在或内容实际上为空才生成，避免覆盖更改过的内容
                    let root = tnx.util.xml.parse(springXmlContent);
                    let configuration = root?.configuration || {};
                    if (!Object.keys(configuration).length) {
                        let templateXmlPath = tnx.util.file.joinPath(tnx.app.getRoot(), 'resources', springXmlFilename);
                        tnx.util.file.read(templateXmlPath).then(xml => {
                            if (!this.model.singleArch) {
                                xml = xml.replaceAll('<FileNamePattern>./logs/', '<FileNamePattern>../logs/');
                            }
                            tnx.util.file.write(springXmlPath, xml);
                        });
                    }
                });
            }

            let localXmlFilename = 'logback-local.xml';
            let localXmlPath = this.getConfigResourcePath(moduleDir, localXmlFilename);
            if (toDelete) {
                tnx.util.file.delete(localXmlPath, true);
            } else {
                tnx.util.file.read(localXmlPath, false, '').then(localXmlContent => {
                    // 文件不存在或内容实际上为空才生成，避免覆盖更改过的内容
                    let root = tnx.util.xml.parse(localXmlContent);
                    let configuration = root?.configuration || {};
                    if (!Object.keys(configuration).length) {
                        let templateXmlPath = tnx.util.file.joinPath(tnx.app.getRoot(), 'resources', localXmlFilename);
                        tnx.util.file.read(templateXmlPath).then(xml => {
                            tnx.util.file.write(localXmlPath, xml);
                        });
                    }
                });
            }

            let filenames = ['application-local.yml'];
            if (project.basic) { // 基础模块将application配置文件至于config目录下
                filenames.unshift('config');
            }
            let localYmlPath = this.getConfigResourcePath(moduleDir, filenames);
            if (toDelete) {
                tnx.util.file.delete(localYmlPath, true);
            } else {
                tnx.util.file.read(localYmlPath, false, '').then(localYmlContent => {
                    let yaml = new tnx.Yaml(localYmlContent);
                    yaml.set(['logging', 'config'], 'classpath:' + localXmlFilename);
                    localYmlContent = yaml.toString();
                    tnx.util.file.write(localYmlPath, localYmlContent);
                });
            }
        },
        saveServiceYml(project, moduleDir) {
            let filenames = ['application.yml'];
            if (project.basic) { // 基础模块将application配置文件至于config目录下
                filenames.unshift('config');
            }
            let ymlPath = this.getConfigResourcePath(moduleDir, filenames);
            tnx.util.file.read(ymlPath, false, '').then(ymlContent => {
                let yml = new tnx.Yaml(ymlContent);
                // 如果没有repo层，则需在service层配置应用名称
                if (!ProjectUtil.containsLayer(project.modules, ModuleLayer.REPO.name)) {
                    yml.setIfAbsent(['spring', 'application', 'name'], project.artifactId);
                }
                let initial = yml.get(['tnxjee', 'enums', 'dict', 'initial']);
                if (initial !== false) {
                    let modelLayerModule = project.modules.find(
                        m => ProjectUtil.getLayerKey(m.artifactId).toUpperCase() === ModuleLayer.MODEL.name);
                    if (modelLayerModule) {
                        let basePackageName = modelLayerModule.groupId + '.model';
                        yml.setIfAbsent(['tnxjee', 'enums', 'dict', 'basePackageNames'], basePackageName);
                    }
                } else {
                    yml.remove(['tnxjee', 'enums', 'dict', 'basePackageNames']);
                }
                ymlContent = yml.toString();
                tnx.util.file.write(ymlPath, ymlContent);
            });
        },
        saveJunitContext(moduleDir, module) {
            let testSupportClass = new CodingClass(module.groupId + '.service.test.ServiceTestSupport');
            testSupportClass.extends =
                [new CodingType('org.truenewx.tnxjee.test.service.support.ServiceSpringTestSupport')];
            let projectRootClass = this.getProjectRootClass();
            testSupportClass.addImportClass('org.springframework.test.context.ActiveProfiles',
                'org.springframework.test.context.ContextConfiguration', 'org.truenewx.tnxjee.Framework',
                projectRootClass.getName());
            testSupportClass.annotations = ['ActiveProfiles("junit")',
                `ContextConfiguration(classes = {Framework.class, ${projectRootClass.simpleName}.class})`];
            let testSupportClassPath = this.getJavaClassPath(moduleDir, testSupportClass.getName(), 'test');
            // 该类内容固定，简单覆盖
            tnx.util.file.write(testSupportClassPath, testSupportClass.toString());

            let schemaSqlPath = this.getConfigResourcePath(moduleDir, ['sql', 'full', 'schema.sql'], 'test');
            tnx.util.file.read(schemaSqlPath, false, '').then(sql => {
                if (!sql.trim()) {
                    sql = 'set foreign_key_checks = 0;\n\n' +
                        'set foreign_key_checks = 1;\n';
                    tnx.util.file.write(schemaSqlPath, sql);
                }
            });

            let junitYmlFilename = 'application-junit.yml';
            let junitYmlPath = this.getConfigResourcePath(moduleDir, junitYmlFilename, 'test');
            tnx.util.file.read(junitYmlPath, false, '').then(junitYmlContent => {
                if (!junitYmlContent.trim()) {
                    let templateJunitYmlPath = tnx.util.file.joinPath(tnx.app.getRoot(), 'resources', junitYmlFilename);
                    tnx.util.file.copy(templateJunitYmlPath, junitYmlPath, true);
                }
            });

            let junitLogbackXmlFilename = 'logback-junit.xml';
            let junitLogbackXmlPath = this.getConfigResourcePath(moduleDir, junitLogbackXmlFilename, 'test');
            tnx.util.file.read(junitLogbackXmlPath, false, '').then(junitLogbackXmlContent => {
                // 文件不存在或内容实际上为空才生成，避免覆盖更改过的内容
                let root = tnx.util.xml.parse(junitLogbackXmlContent);
                let configuration = root?.configuration || {};
                if (!Object.keys(configuration).length) {
                    let templateJunitLogbackXmlPath = tnx.util.file.joinPath(tnx.app.getRoot(), 'resources',
                        junitLogbackXmlFilename);
                    tnx.util.file.read(templateJunitLogbackXmlPath).then(junitLogbackXmlContent => {
                        junitLogbackXmlContent = junitLogbackXmlContent.replaceAll('<logger name="rootPackageName"',
                            `<logger name="${module.groupId}"`);
                        tnx.util.file.write(junitLogbackXmlPath, junitLogbackXmlContent);
                    });
                }
            });
        },
        saveWebProperties(project, projectIndex, moduleDir, module) {
            let filenames = ['application.properties'];
            if (project.basic) { // 基础模块将application配置文件至于config目录下
                filenames.unshift('config');
            }
            let propertiesPath = this.getConfigResourcePath(moduleDir, filenames);
            tnx.util.file.read(propertiesPath, false, '').then(propertiesContent => {
                let properties = new tnx.Properties(propertiesContent);
                // 如果没有repo和service层，则需在web层application.properties写入应用名称
                if (!ProjectUtil.containsLayer(project.modules, ModuleLayer.SERVICE.name)
                    && !ProjectUtil.containsLayer(project.modules, ModuleLayer.REPO.name)) {
                    properties.setIfAbsent('spring.application.name', project.artifactId);
                }
                let dependsOtherModuleLayer = ProjectUtil.dependsOtherModuleLayer(this.model, module);
                if (!dependsOtherModuleLayer) {
                    properties.setIfAbsent('spring.profiles.active', '@profiles.active@');
                }
                if (!module.basic) { // 非基础模块才需要配置端口
                    properties.setIfAbsent('server.port', 8080 + projectIndex);
                }
                if (!dependsOtherModuleLayer) {
                    properties.setIfAbsent('server.servlet.context-path', '/');
                    properties.setIfAbsent('server.servlet.encoding.force', 'true');
                    properties.setIfAbsent('server.servlet.encoding.charset', 'UTF-8');
                    properties.setIfAbsent('server.servlet.session.timeout', '15m');
                    properties.setIfAbsent('server.servlet.session.cookie.same-site', 'Lax');
                    properties.setIfAbsent('spring.servlet.multipart.max-file-size', '100MB');
                    properties.setIfAbsent('spring.servlet.multipart.max-request-size', '1GB');
                    properties.setIfAbsent('spring.mvc.pathmatch.matching-strategy', 'ant_path_matcher');
                    if (ProjectUtil.getSsrEnabled(this.model, module)) {
                        properties.setIfAbsent('spring.mvc.static-path-pattern',
                            '/assets/**, /pages/**/*.css, /pages/**/*.js, /*.html, /*.txt, /libs/**');
                        properties.setIfAbsent('spring.web.resources.static-locations', '/public/, /static/');
                    }
                    if (ProjectUtil.getNacosEnabled(this.model, module)) {
                        properties.setIfAbsent('spring.cloud.nacos.config.server-addr',
                            '${nacos.host:127.0.0.1}:${nacos.port:8848}');
                        properties.setIfAbsent('spring.cloud.nacos.config.namespace', '${spring.profiles.active}');
                        properties.setIfAbsent('spring.cloud.nacos.config.shared-configs', 'common');
                        properties.setIfAbsent('spring.cloud.nacos.discovery.server-addr',
                            '${spring.cloud.nacos.config.server-addr}');
                        properties.setIfAbsent('spring.cloud.nacos.discovery.ip', '${nacos.host:127.0.0.1}');
                        properties.setIfAbsent('spring.cloud.nacos.discovery.namespace', '${spring.profiles.active}');
                    }
                }
                propertiesContent = properties.toString();
                tnx.util.file.write(propertiesPath, propertiesContent);
            });
        },
        // 保存 WebApp 类
        saveWebAppClass(baseModule, parentProject, moduleDir, module) {
            let packageName = module.groupId;
            let simpleName = '';
            let array = parentProject.artifactId.split('-');
            for (let a of array) {
                simpleName += a.firstToUpperCase();
            }
            simpleName += 'WebApp';
            let appClass = new CodingClass(packageName, simpleName);
            let appClassPath = this.getJavaClassPath(moduleDir, appClass.getName());
            tnx.util.file.exists(appClassPath).then(exists => {
                if (parentProject.basic) { // 基础模块，没有 WebApp 类
                    if (exists) {
                        tnx.util.file.delete(appClassPath, true);
                    }
                } else {
                    if (!exists) { // 非基础模块，不存在时才创建，不进行覆盖
                        let scanBaseClass = appClass;
                        appClass.addImportClass('org.springframework.boot.SpringApplication',
                            'org.springframework.boot.autoconfigure.SpringBootApplication',
                            'org.truenewx.tnxjee.Framework',
                            'org.truenewx.tnxjee.core.util.LogUtil');
                        if (!this.model.singleArch) {
                            if (baseModule) {
                                scanBaseClass = this.getProjectRootClass();
                                appClass.addImportClass(scanBaseClass);
                            }
                            appClass.addImportClass('org.springframework.cloud.openfeign.EnableFeignClients');
                            appClass.annotations.push('EnableFeignClients');
                        }

                        appClass.annotations.push(
                            `SpringBootApplication(scanBasePackageClasses = {Framework.class, ${scanBaseClass.simpleName}.class})`);
                        let mainMethod = new CodingMethod('main');
                        mainMethod.modifier = 'public static';
                        let mainMethodParameterType = new CodingType('java.lang.String');
                        mainMethodParameterType.arrayed = true;
                        mainMethod.addParameter(mainMethodParameterType, 'args');
                        mainMethod.body = '    long time0 = System.currentTimeMillis();\n' +
                            `    SpringApplication.run(${simpleName}.class, args);\n` +
                            '    long time1 = System.currentTimeMillis();\n' +
                            `    LogUtil.info(${simpleName}.class, "====== Successfully started ${parentProject.artifactId}, took {} seconds ======", (time1 - time0) / 1000);`;
                        appClass.methods.push(mainMethod);

                        tnx.util.file.write(appClassPath, appClass.toString());
                    }
                }
            });
            return simpleName;
        },
        // 保存 ServletInitializer 类
        saveServletInitializer(parentProject, moduleDir, module, appClassSimpleName) {
            let packageName = module.groupId;
            let servletInitializerClass = new CodingClass(packageName, 'ServletInitializer');
            let servletInitializerClassPath = this.getJavaClassPath(moduleDir, servletInitializerClass.getName());
            tnx.util.file.exists(servletInitializerClassPath).then(exists => {
                if (parentProject.basic) { // 基础模块，没有 ServletInitializer 类
                    tnx.util.file.delete(servletInitializerClassPath, true);
                } else {
                    if (!exists) { // 非基础模块，不存在时才创建，不进行覆盖
                        servletInitializerClass.extends =
                            [new CodingType(
                                'org.springframework.boot.web.servlet.support.SpringBootServletInitializer')];
                        let configureMethod = new CodingMethod('configure');
                        configureMethod.annotations = ['Override'];
                        configureMethod.returnType =
                            new CodingType('org.springframework.boot.builder.SpringApplicationBuilder');
                        configureMethod.addParameter(configureMethod.returnType, 'application');
                        configureMethod.body = `return application.sources(${appClassSimpleName}.class);`;
                        servletInitializerClass.methods.push(configureMethod);

                        tnx.util.file.write(servletInitializerClassPath, servletInitializerClass.toString());
                    }
                }
            });
        },
        saveWebMvcConfigClass(baseModule, parentProject, moduleDir, module) {
            const supportClassSimpleName = 'WebMvcConfigSupport';
            let packageName = module.groupId + '.web.config';
            let simpleName = parentProject.basic ? supportClassSimpleName : 'WebMvcConfig';
            let mvcConfigClass = new CodingClass(packageName, simpleName);
            let mvcConfigClassPath = this.getJavaClassPath(moduleDir, mvcConfigClass.getName());
            tnx.util.file.exists(mvcConfigClassPath).then(exists => {
                if (!exists) {
                    if (parentProject.basic) {
                        mvcConfigClass.modifier = 'abstract';
                    } else {
                        mvcConfigClass.addImportClass('org.springframework.context.annotation.Configuration');
                        mvcConfigClass.annotations = ['Configuration'];
                    }

                    let superClassName = null;
                    if (baseModule && !parentProject.basic) {
                        let baseWebModule = baseModule.modules.find(
                            m => ProjectUtil.getLayerKey(m.artifactId).toUpperCase() === ModuleLayer.WEB.name);
                        if (baseWebModule) {
                            superClassName = baseWebModule.groupId + '.web.config.' + supportClassSimpleName;
                        }
                    }
                    if (!superClassName) {
                        if (ProjectUtil.getSsrEnabled(this.model, module)) {
                            superClassName = 'org.truenewx.tnxjee.webmvc.view.config.WebViewMvcConfigurerSupport';
                        } else {
                            superClassName = 'org.truenewx.tnxjee.webmvc.config.WebMvcConfigurerSupport';
                        }
                    }
                    mvcConfigClass.extends = [new CodingType(superClassName)];

                    tnx.util.file.write(mvcConfigClassPath, mvcConfigClass.toString());
                }
            });
        },
        saveWebSecurityConfigClass(baseModule, parentProject, moduleDir, module) {
            const supportClassSimpleName = 'WebSecurityConfigSupport';
            let packageName = module.groupId + '.web.config';
            let simpleName = parentProject.basic ? supportClassSimpleName : 'WebSecurityConfig';
            let securityConfigClass = new CodingClass(packageName, simpleName);
            let securityConfigClassPath = this.getJavaClassPath(moduleDir, securityConfigClass.getName());
            tnx.util.file.exists(securityConfigClassPath).then(exists => {
                if (!exists) {
                    if (parentProject.basic) {
                        securityConfigClass.modifier = 'abstract';
                    } else {
                        securityConfigClass.addImportClass('org.springframework.context.annotation.Configuration');
                        securityConfigClass.annotations = ['Configuration'];
                    }

                    let superClassName = null;
                    if (baseModule && !parentProject.basic) {
                        let baseWebModule = baseModule.modules.find(
                            m => ProjectUtil.getLayerKey(m.artifactId).toUpperCase() === ModuleLayer.WEB.name);
                        if (baseWebModule) {
                            superClassName = baseWebModule.groupId + '.web.config.' + supportClassSimpleName;
                        }
                    }
                    if (!superClassName) {
                        superClassName =
                            'org.truenewx.tnxjee.webmvc.security.config.annotation.web.configuration.WebMvcSecurityConfigurerSupport';
                    }
                    securityConfigClass.extends = [new CodingType(superClassName)];

                    tnx.util.file.write(securityConfigClassPath, securityConfigClass.toString());
                }
            });
        },
        getJavaClassPath(moduleDir, className, type = 'main') {
            return tnx.util.file.joinPath(moduleDir, 'src', type, 'java',
                className.replace(/\./g, tnx.util.file.separator) + '.java');
        },
        getConfigResourcePath(moduleDir, filename, type = 'main') {
            let filenames = Array.isArray(filename) ? filename : [filename];
            return tnx.util.file.joinPath(moduleDir, 'src', type, 'resources', ...filenames);
        },
        toModeling() {
            if (this.project.modules.length) {
                this.$router.replace('/modeling?pomPath=' + this.path);
            }
        },
    },
};
</script>

<style scoped>
.el-card {
    overflow: auto;
}
</style>
